package com.zte.claa.inficombo.csa.app.service.nbiot.impl;

import java.io.ByteArrayOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeSet;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.script.Invocable;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.zte.claa.inficombo.csa.app.dao.mysql.impl.ProjCustAttrMapper;
import com.zte.claa.inficombo.csa.app.model.dev.DevBase;
import com.zte.claa.inficombo.csa.app.model.proj.ProjCustAttrBean;
import com.zte.claa.inficombo.csa.app.model.redis.ProjectDev;
import com.zte.claa.inficombo.csa.app.service.dev.DevBaseService;
import com.zte.claa.inficombo.csa.app.service.jsplugin.JSParam;
import com.zte.claa.inficombo.csa.app.service.nbiot.NBIotCmdAck;
import com.zte.claa.inficombo.csa.app.service.rptparse.ParserContainer;
import com.zte.claa.inficombo.csa.app.util.HttpClientUtils;
import com.zte.claa.inficombo.csa.app.util.JsonUtils;

/**
 * 电信天翼物联网平台AEP命令下发服务接口
 * 
 * @author 0283000115
 */

@Service
public class NBIotCtccCmdExecService {	
	/**
	 * 根据电信AEP平台描述，如果直接使用API接口对接AEP平台，需要计算时间偏移量，以免因双方时间不同步（偏差过大）导致服务拒绝（time_expired);
	 */
	private Map<String, TimeOffsetItem> timeOffsetMap = new HashMap<String, TimeOffsetItem>();
	
	// 电信AEP平台默认域名.
	private static final String CTCC_AEP_SVRURL = "ag-api.ctwing.cn";
	private static final String CTCC_AEP_APIVERSION = "20190712225145";
	
	/**
	 *  nbiot.ctcc.aepapi.protocol=http
	 	nbiot.ctcc.aepapi.svrurl=ag-api.ctwing.cn
		nbiot.ctcc.aepapi.appkey=xxxxx
		nbiot.ctcc.aepapi.appsecret=yyyyyy
		nbiot.ctcc.aepapi.lwm2m.masterkey=xxxxxxxxxxx
		nbiot.ctcc.aepapi.lwm2m.productid=123456
	 */
	private static final String PROJ_CUST_CTCC_NB_PROTOCOL = "nbiot.ctcc.aepapi.protocol";
	private static final String PROJ_CUST_CTCC_NB_SVRURL = "nbiot.ctcc.aepapi.svrurl";
	private static final String PROJ_CUST_CTCC_NB_APPKEY = "nbiot.ctcc.aepapi.appkey";
	private static final String PROJ_CUST_CTCC_NB_APPSECRET = "nbiot.ctcc.aepapi.appsecret";
	private static final String PROJ_CUST_CTCC_NB_MASKERKEY = "nbiot.ctcc.aepapi.lwm2m.masterkey";
	private static final String PROJ_CUST_CTCC_NB_PRODUCTID = "nbiot.ctcc.aepapi.lwm2m.productid";
	private static final String PROJ_CUST_CTCC_NB_APIVERSION = "nbiot.ctcc.aepapi.apiversion";
	
	// 日志记录
	private final Logger logger = LoggerFactory.getLogger(NBIotCtccCmdExecService.class);		
	
	//@Autowired
	//private BaseCacheManager cacheManager;
	
	@Autowired
	private ParserContainer container;
	
	@Autowired
	private DevBaseService devService;
	
	@Autowired
	private ProjCustAttrMapper projAttrService;
	
	// 针对CTCC-NBIOT设备下发命令
	public NBIotCmdAck execCtccNBIotCmd(DevBase baseDev, String payload, Map<String, Object> extParaMap, boolean offlineMode) throws Exception {
		// 构造请求.
		String devEui = baseDev.getDevEui();
		ProjectDev dev = this.devService.queryProjectDevByEui(devEui);
		// 调用JS插件构造命令
		String aepCmd = this.buildNbIotCmdArgs(dev, payload, extParaMap);		
		return this.execCtccAepApiCmd(baseDev, aepCmd, offlineMode);
	}
	
	// 执行电信天翼AEP命令. 
	private NBIotCmdAck execCtccAepApiCmd(DevBase baseDev, String aepCmd, boolean offlineMode) throws Exception {
		// URL: http://ag-api.ctwing.cn/aep_device_command
		String projectId = baseDev.getProjectId();
		long offset = 0;
		TimeOffsetItem item = this.timeOffsetMap.get(projectId);
		if(item == null || item.hasSync == false){
			offset = this.getAepApiTimeOffset(projectId);
			// timeoffset同步成功. 
			if(offset < Long.MAX_VALUE){
				item = new TimeOffsetItem();
				item.setProjectId(projectId);
				item.setHasSync(true);
				item.setOffset(offset);
				this.timeOffsetMap.put(projectId, item);
			} else {
				offset = 0;
			}
		}
		// 查询配置参数.		
		StringBuffer urlBuf = new StringBuffer();
		String httpProtocol = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_PROTOCOL, "http");
		String svrUrl = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_SVRURL, CTCC_AEP_SVRURL);
		urlBuf.append(httpProtocol).append("://").append(svrUrl).append("/aep_device_command/command");
		
		// "FJDq8agNp5";密钥，到控制台->应用管理打开应用可以找到此值
		String secret = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_APPSECRET, ""); 
		// "91Ebv1S0HBb"; appKey，到控制台->应用管理打开应用可以找到此值
        String application = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_APPKEY, "");  
        // api版本，到文档中心->使能平台API文档打开要调用的api可以找到版本值
        String version = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_APIVERSION, CTCC_AEP_APIVERSION);
        //  "25ce00cc28c1498c833276110ee483f0"; MasterKey，在产品中心打开对应的产品查看此值
        String masterKey = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_MASKERKEY, "");  	
        // productid
        String productIdVal = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_PRODUCTID, "0");  	
        int productId = Integer.parseInt(productIdVal);
        
        // 获取时间戳
        long timestamp = System.currentTimeMillis() + offset;
        Date date = new Date(timestamp);
        SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        // 生成格式化的日期字符串
        String dataString = dateFormat.format(date);
        
        // 生成签名.
        Map<String, String> param = new HashMap<String, String>();
        param.put("MasterKey", masterKey);
        String signature = this.sign(param, timestamp, application, secret, aepCmd.getBytes());
        
        // 准备header中的参数.
        Map<String, String> headerParaMap = new HashMap<String, String>();
		// 公共头参数.
		headerParaMap.put("application", application);
		headerParaMap.put("timestamp", "" + timestamp);
		headerParaMap.put("version", version);
		headerParaMap.put("Content-Type", "application/json; charset=UTF-8");
		headerParaMap.put("Date", dataString);
		headerParaMap.put("signature", signature);
		// createcommand需要的header参数.
		headerParaMap.put("MasterKey", masterKey);

		// 准备命令参数.
		/**
		  {
			  "content": {},
			  "deviceId": "string",
			  "operator": "string",
			  "productId": 0,
			  "ttl": 7200,
			  "deviceGroupId": 100,
			  "level": 1
			}
			
			deviceId: 设备ID，（当指令级别为设备级时必填，为设备组级时则不填）
			operator: 操作者，必填
			productId: 产品ID，必填
			ttl: 设备离线时指令缓存时长，选填。单位：秒，取值范围：0-864000。不携带则默认值：7200。如不需缓存请填0。
			level:指令级别，1或2为设备级别,3为设备组级别，选填。不填默认设备级。
			deviceGroupId：设备组ID，选填，当指令级别为设备级，
			deviceId不为空，deviceGroupId为空；当指令级别为设备组级，deviceId为空，deviceGroupId不为空。
			content: 指令内容，必填，格式为Json，如下（TCP和LWM2M协议透传的content内容）：
		     {
		      payload:指令内容,数据格式为十六进制时需要填十六进制字符串,
		      dataType:数据类型：1字符串，2十六进制
		     }
		 */
		JSONObject contentJson = new JSONObject();
		contentJson.put("payload", aepCmd);
		// 数据类型：1字符串，2十六进制
		contentJson.put("dataType", 1);
		
		// 构造请求body
		JSONObject argsJson = new JSONObject();
		argsJson.put("content", contentJson);
		argsJson.put("deviceId", baseDev.getDevEui());
		argsJson.put("operator", "zteclaa");
		argsJson.put("productId", productId);
		// 非离线模式ttl=10分钟，否则12小时.
		int ttl = 600;
		if(offlineMode){
			ttl = 43200;
		}
		argsJson.put("ttl", ttl);
		argsJson.put("level", 1);
		logger.info("Begin to exec cmd: {}, url={}, reqbody={}", aepCmd.toString(), urlBuf.toString(), argsJson.toString());
		NBIotCmdAck ack = new NBIotCmdAck();
		try{
			String rsp = HttpClientUtils.sendHttpPostJsonWithHeadPara(urlBuf.toString(), argsJson.toString(), headerParaMap, null);
			logger.info("execCtccAepApiCmd rsp: "+rsp);
			/**
			 * {
				  "code": 0,
				  "msg": "ok",
				  "result": {
				    "commandId": "674",
				    "command": "{\"double\":3}",
				    "commandStatus": "指令已保存",
				    "deviceId": "43c148896dcd43ef97826f3f539484f5",
				    "productId": 10000577,
				    "createBy": "test",
				    "createTime": 1542877423798
				  }
				}
			 */
			JSONObject ackJson = new JSONObject(rsp);
			String errNoStr = JsonUtils.getJsonAttrVal(ackJson, "code", "0");
			String error = JsonUtils.getJsonAttrVal(ackJson, "msg", "");
			JSONObject retSubJson = JsonUtils.getSubObject(ackJson, "result");			
			ack.setErrno(Integer.parseInt(errNoStr));
			ack.setError(error);
			if(retSubJson != null){
				ack.setData(retSubJson.toString());
			}
		}catch(Exception e){
			logger.error("execCtccAepApiCmd failed: {}", argsJson.toString(), e);
			// 非onenet的错误码.
			ack.setErrno(2); 
			ack.setError(e.getMessage());
		}
		return ack;
	}

	/**
	 * 构造NBIOT-API的命令参数（request请求body参数字符串）；
	 * 
	 * @return
	 * @throws Exception
	 */
	private String buildNbIotCmdArgs(ProjectDev dev, String payload, Map<String, Object> extParaMap) throws Exception {
		String devTypeId = dev.getDevtype();
		Invocable jsRunner = this.container.getJsPnpService().getJsPLugin(devTypeId);
		if(jsRunner == null){
			logger.error("[*!!*]Can not find the JS plugin for devtype="+devTypeId);
			throw new Exception("Can not find the JS plugin for devtype="+devTypeId);
		}		
		JSONObject json = new JSONObject(payload);
		String cmdCode = JsonUtils.getJsonAttrVal(json, "cmd", null);
		if(cmdCode == null){
			logger.error("[*@@*]Can not get cmd code from payload[cmd]: "+payload);
			throw new Exception("Can not get cmd code from payload[cmd]: "+payload);
		}
		Map<String, Object> cmdParaMap = null;
		JSONObject jsonParaObj = JsonUtils.getSubObject(json, "paras");
		if(jsonParaObj == null){
			cmdParaMap = new HashMap<String, Object>(0);
		} else {
			cmdParaMap = JsonUtils.jsonStrToHashMap(jsonParaObj.toString());
		}
		JSParam jsParam = new JSParam();
		jsParam.setDeveui(dev.getDeveui());
		jsParam.setDevtypeid(devTypeId);
		jsParam.setDevport("0");
		jsParam.setParas(cmdParaMap);
		if(extParaMap != null){
			jsParam.setDevExtDatas(extParaMap);
		}
		byte[] pbs = (byte[])jsRunner.invokeFunction(cmdCode, jsParam);	
		if(pbs == null || pbs.length == 0){
			throw new Exception("下发命令构造错误，请检查命令参数设置！");
		}
		String str = new String(pbs, "UTF-8");
		logger.info("Cmd args build ret: {}, origin-payload={}", str, payload);
		return str;
	}
	
	/**
	 * 查询项目定制化参数.
	 * 
	 * @return
	 */
	private String getProjCustAttrPara(String projectId, String attrName, String defValue) {
		try{
			ProjCustAttrBean attrBean = this.projAttrService.queryProjCustAttrBean(projectId, attrName);
			if(attrBean != null){
				return attrBean.getAttrvalue();
			}			
		}catch(Exception e){
			logger.error("GetProjCustAttrPara [{}] failed!", attrName, e);
		}
		return defValue;
	}
	
	/**
     * 请求签名
     * 
     * @param param    api 配置参数表
     * @param timestamp UNIX格式时间戳
     * @param application appKey,到应用管理打开应用可以找到此值
     * @param secret 密钥,到应用管理打开应用可以找到此值
     * @param body 请求body数据,如果是GET请求，此值写null
     * @return 签名数据
     */
	public String sign(Map<String, String> param, long timestamp, String application, String secret, byte[] body) throws Exception {
		// 连接系统参数
		StringBuffer sb = new StringBuffer();
		sb.append("application").append(":").append(application).append("\n");
		sb.append("timestamp").append(":").append(timestamp).append("\n");

		// 连接请求参数
		if (param != null) {
			TreeSet<String> keys = new TreeSet<String>(param.keySet());
			Iterator<String> i = keys.iterator();
			while (i.hasNext()) {
				String s = i.next();
				String val = param.get(s);
				sb.append(s).append(":").append(val == null ? "" : val).append("\n");
			}
		}

		// body数据写入需要签名的字符流中
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		baos.write(sb.toString().getBytes("utf-8"));
		if (body != null && body.length > 0) {
			baos.write(body);
			baos.write("\n".getBytes("utf-8"));
		}

		// 得到需要签名的字符串
		String string = baos.toString();
		logger.info("Sign string: " + string);

		// hmac-sha1编码
		byte[] bytes = null;
		SecretKey secretKey = new SecretKeySpec(secret.getBytes("utf-8"), "HmacSha1");
		Mac mac = Mac.getInstance(secretKey.getAlgorithm());
		mac.init(secretKey);
		bytes = mac.doFinal(string.getBytes("utf-8"));

		// base64编码
		String encryptedString = new String(Base64.encodeBase64(bytes));

		// 得到需要提交的signature签名数据
		return encryptedString;
	}
	
	/**
	 * 开发者可以通过http://${正式环境地址}/echo接口的响应报文头的timestamp字段获得服务器时间t1，
	 * 在服务器返回的同时获得本地时间t2，计算出大致的时间偏移量: 
	    offset = t1-t2
		在后续的请求，公共参数中需要提供的timestamp参数，需要根据这个偏移量进行修正后再写入到请求或签名中。
		timestamp = System.currentTimeMillis() + offset
	 * @return
	 */
	private long getAepApiTimeOffset(String projectId) {
		// "http://ag-api.ctwing.cn/echo";
		StringBuffer urlBuf = new StringBuffer();
		String httpProtocol = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_PROTOCOL, "http");
		String svrUrl = this.getProjCustAttrPara(projectId, PROJ_CUST_CTCC_NB_SVRURL, CTCC_AEP_SVRURL);
		urlBuf.append(httpProtocol).append("://").append(svrUrl).append("/echo");

		long offset = 0;
		HttpResponse response = null;
		// 构造httpGet请求
		CloseableHttpClient httpClient = HttpClientBuilder.create().build();
		HttpGet httpTimeGet = new HttpGet(urlBuf.toString());
		try {
			long start = System.currentTimeMillis();
			response = httpClient.execute(httpTimeGet);
			long end = System.currentTimeMillis();
			// 时间戳在返回的响应的head的x-ag-timestamp中
			Header[] headers = response.getHeaders("x-ag-timestamp");
			if (headers.length > 0) {
				long serviceTime = Long.parseLong(headers[0].getValue());
				offset = serviceTime - (start + end) / 2L;
			}			
		} catch (Exception e) {
			logger.error("getAepApiTimeOffset failed!", e);
			offset = Long.MAX_VALUE;
		} finally {
			try{
				httpTimeGet.releaseConnection();
				httpTimeGet = null;
			}catch(Exception ex){
				logger.error("httpTimeGet.releaseConnection() failed!", ex);
			}
			try{
				httpClient.close();
				httpClient = null;
			}catch(Exception ex){
				logger.error("httpClient.close() failed!", ex);
			}
		}
		return offset;
	}
	
	public class TimeOffsetItem {
		private String projectId;
		private boolean hasSync = false;
		private long offset = 0;
		
		public TimeOffsetItem(){
			
		}

		public String getProjectId() {
			return projectId;
		}

		public void setProjectId(String projectId) {
			this.projectId = projectId;
		}

		public boolean isHasSync() {
			return hasSync;
		}

		public void setHasSync(boolean hasSync) {
			this.hasSync = hasSync;
		}

		public long getOffset() {
			return offset;
		}

		public void setOffset(long offset) {
			this.offset = offset;
		}
		
	}
}

